Skip to content

Conversation

@joelwurtz
Copy link

Hey,

I think an example illustrate more than a long text, so here is a PR of something we need : associate metadata with a response in cache.

We need this because we have a middleware than runs after the cache which :

  • compute some information based on the request / response
  • may add cache headers to the response (it's wanted)
  • log this process (with the computed information)

We still want to log, even if we cache, so we will extract the last part into an other middleware that will run before the cache, however we don't want to recompute some informations needed as it can be costly and will always be the same for a given cache.

We do think saving this information with the response cache would resolve our need, there may be other use cases where having the possibility to have metadata in the response could be useful (as an exemple if you use response extensions, you may want to store them inside cache).

About the implementation i used a Vec<u8> so metadata will need to be serialized / deserialized by the user (so there is double serialization involved with the HttpResponse also being serialized)

I studied the possibility to have

HttpResponse<M> {
    ...
    metadata: M,
}

But it need a lot of refactoring to handle this everywhere and add some complexity since we will need to add many restrictions on this generic type (being Serializable / Deserializable / Send / Sync / ...) i prefer to show this before going there if needed.

@06chaynes
Copy link
Owner

I could see this being useful, though I think it might be worth wrapping the value in an Option

@joelwurtz joelwurtz force-pushed the feat/response-metadata branch from 80b589a to 13c8df0 Compare November 3, 2025 08:57
@joelwurtz
Copy link
Author

joelwurtz commented Nov 3, 2025

I wrapped the metadata in an Option, i agree it's better in term of DX, also updated underlying library to the new process_response API.

At the moment passing metadata is only possible if we use directly the http-cache library. Should we propose a way to do it for middlewares also ?

We could propose a

pub type HttpCacheMetadata = Vec<u8>

So end-user could add this type to its response extension. What would be better it's automatically pass all extensions values of the response into the metadata, but not sure if this possible.

@06chaynes
Copy link
Owner

I wrapped the metadata in an Option, i agree it's better in term of DX, also updated underlying library to the new process_response API.

At the moment passing metadata is only possible if we use directly the http-cache library. Should we propose a way to do it for middlewares also ?

We could propose a

pub type HttpCacheMetadata = Vec<u8>

So end-user could add this type to its response extension. What would be better it's automatically pass all extensions values of the response into the metadata, but not sure if this possible.

I could have sworn I responded to this but I guess it never sent. I think some integration at the client end makes sense. With reqwest there's this https://docs.rs/reqwest-middleware/latest/reqwest_middleware/struct.Extension.html, maybe something could be integrated there?

@codecov
Copy link

codecov bot commented Nov 17, 2025

Codecov Report

❌ Patch coverage is 93.22034% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.54%. Comparing base (1ff54c2) to head (2c8d97b).
⚠️ Report is 43 commits behind head on main.

Files with missing lines Patch % Lines
http-cache-tower/src/lib.rs 77.77% 2 Missing ⚠️
http-cache/src/lib.rs 86.66% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #120      +/-   ##
==========================================
- Coverage   93.91%   85.54%   -8.37%     
==========================================
  Files          10       19       +9     
  Lines        1233     7362    +6129     
==========================================
+ Hits         1158     6298    +5140     
- Misses         75     1064     +989     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@06chaynes
Copy link
Owner

I think we could do something like this:

Add a metadata callback to HttpCacheOptions
pub type MetadataProvider = Arc<
    dyn Fn(&request::Parts, &response::Parts) -> Option<Vec<u8>> + Send + Sync
>;

pub struct HttpCacheOptions {
    // ... existing fields
    pub metadata_provider: Option<MetadataProvider>,
}

Which works along the same ways as the cache mode function and whatnot

@06chaynes
Copy link
Owner

@joelwurtz Added that implementation along with some tests to serve as examples, seems to work pretty well. Curious if you believe this solution would meet the requirements on your end

@joelwurtz
Copy link
Author

joelwurtz commented Nov 22, 2025

I think it should work, as i already pass compute logic with the response extension API, we can even remove the metadata argument to the process_response which would avoid a bc break then ?

However i don't see how users could retrieve those Metadata when using one of the middleware ? I think it should be automatically added back to the extension of the response, maybe i miss it ?, so he could do :

let metadata_as_bytes = response.extensions().get::<HttpCacheMetadata>();

I will branch my project on this branch next week to ensure that it works nicely.

Thanks a lot for the follow up.

@06chaynes
Copy link
Owner

Made a few adjustments and currently the flow looks about like:

use http_cache_reqwest::{Cache, CacheMode, HttpCache, HttpCacheOptions};
use http_cache::{CACacheManager, HttpCacheMetadata};
use reqwest::Client;
use reqwest_middleware::ClientBuilder;
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create cache options with a metadata provider
    let options = HttpCacheOptions {
        metadata_provider: Some(Arc::new(|request_parts, response_parts| {
            // Generate metadata based on request/response
            // Example: Store the ETag and Last-Modified for later use
            let mut metadata = Vec::new();
            
            if let Some(etag) = response_parts.headers.get("etag") {
                metadata.extend_from_slice(b"etag:");
                metadata.extend_from_slice(etag.as_bytes());
                metadata.push(b'\n');
            }
            
            if let Some(last_modified) = response_parts.headers.get("last-modified") {
                metadata.extend_from_slice(b"last-modified:");
                metadata.extend_from_slice(last_modified.as_bytes());
            }
            
            if metadata.is_empty() {
                None
            } else {
                Some(metadata)
            }
        })),
        ..Default::default()
    };

    // Build the client with caching
    let client = ClientBuilder::new(Client::new())
        .with(Cache(HttpCache {
            mode: CacheMode::Default,
            manager: CACacheManager::default(),
            options,
        }))
        .build();

    // Make a request
    let response = client
        .get("https://api.example.com/data")
        .send()
        .await?;

    // Check if we got cached metadata
    if let Some(metadata) = response.extensions().get::<HttpCacheMetadata>() {
        let metadata_str = String::from_utf8_lossy(metadata.as_slice());
        println!("Cache metadata: {}", metadata_str);
        
        // Parse and use the metadata as needed
        for line in metadata_str.lines() {
            if let Some(etag) = line.strip_prefix("etag:") {
                println!("Cached ETag: {}", etag);
            }
            if let Some(last_modified) = line.strip_prefix("last-modified:") {
                println!("Cached Last-Modified: {}", last_modified);
            }
        }
    } else {
        println!("No cache metadata (cache miss or no metadata stored)");
    }

    // Use the response body as normal
    let body = response.text().await?;
    println!("Response: {}", body);

    Ok(())
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants